AnchoredOverlay: Add Popover API, modifications to CSS anchor positioning#7677
AnchoredOverlay: Add Popover API, modifications to CSS anchor positioning#7677
Conversation
🦋 Changeset detectedLatest commit: d7af05c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
b2ee25e to
b55ac54
Compare
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
There was a problem hiding this comment.
Pull request overview
This PR introduces Popover API support to AnchoredOverlay (behind the primer_react_css_anchor_positioning feature flag) and updates the CSS anchor positioning approach, including adding an opt-out for rendering overlays in a Portal.
Changes:
- Add
disablePortalsupport toOverlayto allow rendering without a Portal when desired. - Update
AnchoredOverlayto integrate Popover API (popover="manual",showPopover()), and to set anchor/position anchor styles via JS. - Extend unit/e2e coverage and Storybook examples (including a “Multiple Overlays” scenario) and add a changeset for a minor release.
Reviewed changes
Copilot reviewed 8 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/Overlay/Overlay.tsx | Adds disablePortal prop and uses it to bypass Portal rendering. |
| packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx | Adds Popover API wiring + JS-managed anchor-name / position-anchor setup. |
| packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx | Updates tests for new portal behavior and feature-flag-specific behavior. |
| packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css | Removes wrapper/anchor classes; adjusts anchored overlay styles for popover behavior. |
| packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx | Adds “Multiple Overlays” story to validate multiple instances. |
| packages/react/src/ActionMenu/ActionMenu.test.tsx | Removes assertions tied to the removed .Anchor CSS class. |
| e2e/components/AnchoredOverlay.test.ts | Adds multi-overlay screenshot coverage. |
| .playwright/snapshots/** | Adds new snapshots for the “Multiple Overlays” story. |
| .changeset/soft-pianos-carry.md | Declares a minor release for the new AnchoredOverlay behavior behind the flag. |
| renderAnchor={props => <Button {...props}>Anchor Button</Button>} | ||
| onPositionChange={onPositionChange} | ||
| className={className} | ||
| {...overlayProps} |
There was a problem hiding this comment.
AnchoredOverlayTestComponent is spreading the overlayProps test setting directly onto <AnchoredOverlay> ({...overlayProps}), which passes disablePortal as a top-level prop instead of as overlayProps={...}. This should be passed via the overlayProps prop (e.g. overlayProps={overlayProps}), otherwise TS should reject it and the test won't be exercising the intended behavior.
| {...overlayProps} | |
| overlayProps={overlayProps} |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
liuliu-dev
left a comment
There was a problem hiding this comment.
Looks great! ✨
Just a few questions to make sure I understand it correctly.
| onClick: onAnchorClick, | ||
| onKeyDown: onAnchorKeyDown, | ||
| ...(cssAnchorPositioning ? {className: classes.Anchor} : {}), | ||
| ...(cssAnchorPositioning ? {'data-anchor': '', popoverTarget: popoverId} : {}), |
There was a problem hiding this comment.
I couldn't find data-anchor used anywhere in the CSS, is this for future styles?
There was a problem hiding this comment.
You're right! We used to use it, but I removed it in a previous commit. I can go ahead and remove it!
| // Track the overlay element so we can re-run the effect when it changes. | ||
| // This is necessary when using a Portal, as React re-renders can replace | ||
| // the DOM node, causing the popover to lose its :popover-open state. | ||
| const overlayElement = overlayRef.current | ||
|
|
||
| useLayoutEffect(() => { | ||
| // Read ref inside effect to get the value after child refs are attached | ||
| const currentOverlay = overlayRef.current | ||
|
|
||
| if (!cssAnchorPositioning || !open || !currentOverlay) return | ||
| currentOverlay.style.setProperty('position-anchor', `--anchored-overlay-anchor-${id}`) | ||
| try { | ||
| if (!currentOverlay.matches(':popover-open')) { | ||
| currentOverlay.showPopover() | ||
| } | ||
| } catch { | ||
| // Ignore if popover is already showing or not supported | ||
| } | ||
| }, [cssAnchorPositioning, open, overlayElement, id, overlayRef]) |
There was a problem hiding this comment.
Curious about this part. Is this because when a Portal remounts, it creates a new Overlay element and :popover-open is lost on this new element?
There was a problem hiding this comment.
Yes exactly! In steps this is how it works:
- The user clicks the trigger button for
AnchoredOverlaywhich turnsopentotrue - We check if the overlay ref is not null (if it's null then the Overlay hasn't rendered yet)
- We then check if it's open (along with other conditionals such as if the feature flag is enabled)
- Lastly, we check if it's somehow open already, as we're using the popover api, we can do this by checking if
:popover-openis present. This guards us from re-opening the overlay multiple times whenever one of the dependencies change.
Let me know if this makes sense, or if you have questions!
There was a problem hiding this comment.
Makes sense, and it's clearer after reading the conversation you and @siddharthkp had, thanks!
siddharthkp
left a comment
There was a problem hiding this comment.
Posted some clarifying questions first
| position-visibility: anchors-visible; | ||
| z-index: 100; | ||
| position: fixed; | ||
| position: fixed !important; |
There was a problem hiding this comment.
curious, why do we need important here? Are there any overrides in dotcom that this might break?
There was a problem hiding this comment.
There was a case where position was being overridden (example: https://github.com/github/github-ui/pull/16228). The !important will most likely be temporary, as we can probably make the selector take precedence in regards to specificity.
|
|
||
| type ContainerProps = { | ||
| anchorSide?: AnchorSide | ||
| disablePortal?: boolean |
There was a problem hiding this comment.
Not sure if we should make this prop part of the public API. Do we want folks to use it at an instance level? Should this be an invisible detail of using the feature flag?
There was a problem hiding this comment.
Yeah that's fair! This one is more so to test when/if we need the portal. Most of the cases where a portal came in handy was with styling. Any styling that the parent had would leak into the AnchoredOverlay, which the portal prevents.
I'll adjust this so that it's only useable with the flag along with making it "private".
| }, [cssAnchorPositioning, anchorRef, overlayRef, id]) | ||
|
|
||
| // Track the overlay element so we can re-run the effect when it changes. | ||
| // This is necessary when using a Portal, as React re-renders can replace |
There was a problem hiding this comment.
Curious about this comment because we seem to only want to use the ref when using css anchor positioning and popover, so not using a portal. Am I reading this wrong?
There was a problem hiding this comment.
You're right. I'll adjust the comment 😅
This is trying to say that if the overlay is mounted/unmounted the popover itself is no longer open (via :popover-open). When it is mounted or re-rendered, we need to open it manually again via showPopover (if open is true) which we need the active ref for. Let me know if this makes sense or not!
There was a problem hiding this comment.
Ohhh okay. I understand this in theory, when does this happen in prod?
There was a problem hiding this comment.
I was running into an issue with Issues SelectPanel, where it would render the SelectPanel only after the initial click. It would be within the DOM but it wouldn't be opened as expected.
On first click, the component would mount and attach its ref. The useLayoutEffect runs with the new DOM node, checks that it's not already opened (via :popover-open), and calls showPopover() to actually display it. Without the effect, the element would exist in the DOM with but never be shown.
Since we're using popover='manual' we need to explicitly use showPopover() to display the popover. The effect ensures we call it whenever a new DOM node is created, whether from the initial open, or from dynamic/lazy mounting where the element gets recreated (such as Issues SelectPanel).
Let me know if I can go into more details too! I'll see about adding context on the Issues SelectPanel case which made me layer the effect this way in https://github.com/github/primer/issues/6321!
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/17094 |
Prerequisite to https://github.com/github/primer/issues/6448, part of https://github.com/github/primer/issues/6431
Utilizes the Popover API to power our CSS anchor positioning in
AnchoredOverlay. Both the popover support and CSS anchor positioning are behind theprimer_react_css_anchor_positioningfeature flag.Changelog
New
AnchoredOverlayChanged
Rollout strategy
Testing & Reviewing
Merge checklist